<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <title>骄傲的贪吃蛇</title>
    <style>
        body {
            background-color: #000000;
            margin: 0px;
            overflow: hidden;
        }
    </style>
</head>

<body>

<script src="https://d3js.org/d3.v5.min.js"></script>
<!--<script src="js/d3.v5.min.js"></script>-->
    
<script>

    d3.selection.prototype.moveToBack = function() {
        return this.each(function() {
            var firstChild = this.parentNode.firstChild;
            if (firstChild) {
                this.parentNode.insertBefore(this, firstChild);
            }
        });
    };

    cout(d3.selection);

    window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame
		|| window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;

var winW = window.innerWidth, winH = window.innerHeight;
var margin = {top:10, right:10, bottom:10, left:10};
var outW = winW - margin.left - margin.right,
	outH = winH - margin.top - margin.bottom;
//// 单元格宽度，高度，外墙尺寸
var tdW = 20, tdH = 20, 
	wallH = (outH % tdH) / 2,          // + tdH * 0 增加高度
	wallW = (outW % tdW) / 2;          // + tdW * 0 增加宽度
	
var zone = {
	cellW: tdW,
	cellH: tdH,
	row: parseInt(outH / tdH),         // - 1 * 0  与墙对应
	col: parseInt(outW / tdW),         // - 1 * 0  与墙对应
	left: margin.left + wallW,
	top: margin.top + wallH,
	right: winW - margin.right - wallW,
	bottom: winH - margin.top - wallH,
	foodSize: 52		// 食物svg真实尺寸
};
var colorSnake = "#71C671";


	// cout(zone  ); 
	// cout(outH);
	//cout([W, H, zone])
var svg = d3.select("body")
		.append("svg")
		.attr("width", winW)
		.attr("height", winH)
		.append("g");

var rows = d3.range(zone.row),
	cols = d3.range(zone.col);

var x= d3.scaleLinear().domain([0, zone.col]).range([zone.left, zone.right]), 
    y= d3.scaleLinear().domain([0, zone.row]).range([zone.top, zone.bottom]);




addGrid();
var snake = new Snake(zone);


//// 增加外围墙
function addWall(){
	var dt = [
		[margin.left, margin.top, wallW, outH], //左
		[margin.left, margin.top, outW, wallH], //上
		[margin.left, zone.bottom, outW, wallH], //下
		[zone.right, margin.top, wallW, outH]	//右
	];
    var walls = svg.append("g");
    walls.selectAll("rect")
		.data(dt)
		.enter()
		.append('rect')
		.attr('x', function(d){ return d[0]; })
		.attr('y', function(d){ return d[1]; })
		.attr('width', function(d){ return d[2]; })
		.attr('height', function(d){ return d[3]; })
		.style('fill', '#98F5FF');
    return walls;
}

//// 增加网格
function addGrid(){
	svg.append("g").selectAll("line")
			.data(d3.range(zone.col + 1))
			.enter()
			.append("line")
			.attr("x1", x)
			.attr("x2", x)
			.attr("y1", zone.top)
			.attr("y2", zone.bottom)
			.style("stroke", "#0ff");

	svg.append("g").selectAll("line")
			.data(d3.range(zone.row + 1))
			.enter()
			.append("line")
			.attr("y1", y)
			.attr("y2", y)
			.attr("x1", zone.left)
			.attr("x2", zone.right)
			.style("stroke", "#0ff");
}

//// apple svg
function createApple(){
    var apple = svg.append('g');
    apple.append('path')
        .attr('d', 'M40.47,8.458c-2.562-1.642-7.374-3.93-11.997-1.816c-0.445,0.204-0.952-0.117-0.952-0.607V1.786C27.522,0.804,26.855,0,26.04,0h-2.223c-0.815,0-1.482,0.804-1.482,1.786v4.501c0,0.425-0.385,0.747-0.802,0.661c-1.877-0.387-6.751-0.989-11.412,1.795c-0.638,0.381-0.268,1.381,0.464,1.247c2.17-0.397,5.026-0.67,6.956,0.092c0.506,0.199,0.579,0.891,0.124,1.189c-1.371,0.895-3.9,2.953-5.557,6.737c-0.282,0.644,0.51,1.221,1.053,0.774c2.117-1.744,5.6-4.107,8.554-3.726c0.348,0.045,0.612,0.329,0.607,0.68c-0.03,1.982-0.005,8.716,1.632,11.265c0.258,0.402,0.836,0.422,1.117,0.035c1.043-1.433,3.304-5.233,3.211-11.167c-0.006-0.39,0.307-0.707,0.697-0.694c1.49,0.048,5.008,0.469,7.798,3.194c0.457,0.447,1.214,0.061,1.134-0.573c-0.219-1.735-1.174-4.359-4.631-6.394c-0.525-0.309-0.436-1.095,0.155-1.24c1.194-0.293,3.252-0.572,6.644-0.46C40.768,9.723,41.049,8.829,40.47,8.458z')
        .style('fill', '#88C057').attr('class', 'foodLeaf');
    apple.append('path')
        .attr('d', 'M41.248,9.99c-0.093-0.065-0.201-0.106-0.314-0.12c-0.4-0.049-0.801-0.095-1.201-0.149  c-0.143-0.014-0.287-0.025-0.429-0.039c-2.914-0.048-4.743,0.206-5.846,0.474c-0.599,0.146-0.707,0.931-0.175,1.244  c3.457,2.035,4.411,4.659,4.63,6.393c0.08,0.634-0.677,1.02-1.134,0.573c-2.79-2.724-6.308-3.145-7.798-3.194  c-0.39-0.013-0.704,0.304-0.697,0.694c0.092,5.934-2.168,9.734-3.211,11.167c-0.282,0.387-0.859,0.367-1.117-0.035  c-1.637-2.549-1.662-9.283-1.632-11.265c0.005-0.35-0.259-0.635-0.607-0.68c-2.954-0.382-6.437,1.982-8.554,3.726  c-0.543,0.447-1.335-0.13-1.053-0.774c1.655-3.779,4.18-5.836,5.552-6.733c0.457-0.299,0.381-0.994-0.128-1.19  c-0.371-0.143-0.776-0.249-1.203-0.324c-0.076-0.014-0.157-0.015-0.234-0.004c-2.425,0.33-4.807,0.791-7.119,1.411  c-0.102,0.027-0.197,0.076-0.278,0.144c-5.103,4.246-8.307,10.359-8.307,17.156c0,12.821,11.393,23.214,25.446,23.214  s25.446-10.393,25.446-23.214C51.286,20.928,47.348,14.23,41.248,9.99z')
        .style('fill', '#D13834').attr('class', 'foodBody');
    apple.append('path')
        .attr('d', 'M5.791,34.636c-0.342,0-0.675-0.175-0.861-0.49C4.182,32.883,3.692,31.488,3.473,30 c-0.08-0.547,0.298-1.055,0.844-1.135c0.556-0.086,1.055,0.298,1.135,0.844c0.178,1.213,0.593,2.395,1.199,3.418 c0.281,0.475,0.125,1.089-0.351,1.37C6.14,34.591,5.964,34.636,5.791,34.636z')
        .style('fill', '#ED7161');
    apple.append('path')
        .attr('d', 'M4.588,26.678c-0.023,0-0.047-0.001-0.07-0.003c-0.552-0.038-0.967-0.516-0.929-1.066 c0.094-1.327,0.366-2.616,0.811-3.834c0.189-0.519,0.761-0.783,1.283-0.596c0.519,0.189,0.785,0.764,0.596,1.283 c-0.381,1.041-0.614,2.146-0.694,3.285C5.548,26.275,5.109,26.678,4.588,26.678z')
        .style('fill', '#ED7161');
    
    return apple;
} 



d3.selection.prototype.moveToFront = function() {
    return this.each(function(){
        this.parentNode.appendChild(this);
    });
};


//// ----------------------------------------Class--------------------------------------------
function Snake(zone){
	//// value : 0-上，1-右，2-下，3-左
	this.direction = {
		head: 0,
		tail: 0
	};
	this.data = [];
	this.objects = {};
    this.foods = {};
    this.foodCount = 100;
    this.foodIndex = 0;     // 吃掉的
	this.stop = true;
	this.interval = 100;
	var $this = this, tm = 0, control = false;
	var config = zone, bodyGroup = svg.append("g");
	//// 食物索引对应方格（螺旋放置吃掉的食物）
    var h = new Helix(config.col, config.row);
    var cacheHelix = h.elements;
    var cacheLayers = h.layers.reduce(function(a, b, i){ a[b] = i + 1; return a; }, {});
    var limit = { minCol:0, minRow:0, maxCol:config.col - 1, maxRow:config.row -1 };

    var walls = addWall();

    
	//// 主动画循环
	this.Animation = function (t){
		if($this.stop)return;

		if((t - tm) > $this.interval){
			tm = t;
			$this.move();
		}
		requestAnimationFrame($this.Animation);
	};
	requestAnimationFrame($this.Animation);
    
    
    this.createFood = function(){
        // 避免食物投放太多
        if((limit.maxCol - limit.minCol) * (limit.maxRow - limit.minRow)/2 < this.foodCount)return -1;
        var e = this.getFoodPos(), pos = getPos(e);
        this.foods[e] = { 'food':createApple(), 'object':createBody(e) };
        this.foods[e].food.attr("transform",
                'translate('+ (pos.x - config.cellW/3.2) +', '+ (pos.y - config.cellH/3.2) +')' +
                'scale('+ config.cellW/config.foodSize * 1.5  +')');
        return e;
    };

    this.getFoodPos = function(){
        while(1){
            // 在有效范围内随机
            var x = parseInt(Math.random() * (limit.maxCol - limit.minCol + 1) + limit.minCol);
            var y = parseInt(Math.random() * (limit.maxRow - limit.minRow + 1) + limit.minRow);
            var r = y * config.col + x;
            // 排除已有食物的位置和动物身体的位置
            if(!this.foods[r] && this.data.indexOf(r)==-1) return r;
        }
    };

    //// 判断吃食物，及食后相关操作
	this.eatFood = function (e){
		if(!this.foods[this.data[0]])return null;
        var block = this.foods[e].object;

        var pos = getPos(cacheHelix[this.foodIndex])
        //// 吃掉食物以此排列，伴随动画
        this.foods[e].food.transition()
                .duration(500)
                .attr("transform",
                'translate('+ (pos.x) +', '+ (pos.y) +')' +
                'scale('+ config.cellW/config.foodSize  +')')
                .select('.foodLeaf').style('fill', '#6A5ACD'); //吃掉后换个颜色

        this.foods[e] = null;
        this.createFood();

        //// 检测食物是否积累一圈，是则围墙向内扩张一圈
        if(this.foodIndex in cacheLayers){
            this.fillWall(cacheLayers[this.foodIndex]);
        }

        //// 蛇身过长时消减一半
        if(this.data.length > 16 || this.data.length > (limit.maxRow - limit.minRow + limit.maxCol - limit.minCol)){
            var halfL = parseInt(this.data.length / 2), tail = this.data[halfL];
            this.objects[tail].remove();
            this.objects[tail] = this.objects[this.data[this.data.length - 1]];
            for(var i=0; i<halfL; i++){
                var d = this.data.pop();
                if(i!=0)this.objects[d].remove();
                this.objects[d] = null;
            }
            controlDirection(this.objects[tail], this.direction.tail, tail);

            //// 加快速度
            var det = (20 - this.foodIndex/config.row);
            if(this.interval>70)this.interval -= (det<5 ? 5 : det);
        }
        this.foodIndex++;
        return block;
	};


    //// 围墙向内扩张一圈
    this.fillWall = function(i){
        var w = limit.maxCol - limit.minCol - 1, h = limit.maxRow - limit.minRow + 1;
        var lt = { x: config.left + i * config.cellW, y: config.top + i * config.cellH }; //左上角
        var dt = [
            [lt.x, lt.y, config.cellW, config.cellH * h], //左
            [lt.x + config.cellW, lt.y, config.cellW * w, config.cellH], //上
            [lt.x + config.cellW * (w + 1), lt.y, config.cellW, config.cellH * h], //右
            [lt.x + config.cellW, lt.y + config.cellH * (h - 1), config.cellW * w, config.cellH], //下
        ];

        walls.append("g").selectAll("rect")
                .data(dt)
                .enter()
                .append('rect')
                .attr('x', function(d){ return d[0]; })
                .attr('y', function(d){ return d[1]; })
                .attr('width', function(d){ return d[2]; })
                .attr('height', function(d){ return d[3]; })
                .style('fill', '#00CED1')
                //.lower();
        limit.minCol++;
        limit.maxCol--;
        limit.minRow++;
        limit.maxRow--;

        // 回收无效食物
        for(var k in this.foods){
            if(!this.foods[k])continue;
            var pos = getXY(k);
            if(pos.x<limit.minCol || pos.x>limit.maxCol || pos.y<limit.minRow || pos.y>limit.maxRow){
                this.foods[k].object.remove();
                this.foods[k].food.remove();

                this.foods[k] = null;
                this.createFood();
            }

        }
    };

    // 初始化
	this.init = function (){
		//// 蛇头方向随机，蛇尾方向相反
		this.direction.head = parseInt(Math.random() *4);
		this.direction.tail = opposite(this.direction.head);
		//// 身体位置位于中间行，随机列（该行前、后3格除外）,  头和尾根据方向计算
		var m = parseInt(config.row / 2) * config.col + parseInt(Math.random() * (config.col - 6) + 3);
		var h = calcMove(m, this.direction.head), t = calcMove(m, this.direction.tail);
		this.data.push(h, m, t);

		this.objects[m] = createBody(m);
		this.objects[h] = createHead(h);
		this.objects[t] = createTail(t);
		controlDirection(this.objects[h], this.direction.head, h);
		controlDirection(this.objects[t], this.direction.tail, t);
		changePos(this.objects[m], m);

        //// 先填一圈用于放食物
        this.fillWall(0);
        //// 随机投放食物
        for(var i=0; i<this.foodCount; i++){
            //var r = this.getFoodPos();
            this.createFood();
        }
		this.bind();
		this.stop = false;
	};

    //// 控制方向操作
	this.bind = function(){
		var $this = this, ds = {37:3, 38:0, 39:1, 40:2};  
		document.onkeydown = function(e) {
		    e = e || window.event;
		    if(!(e.keyCode in ds) || Math.abs(ds[e.keyCode] - $this.direction.head)==2)return;
		    if(control){
		    	$this.direction.head = ds[e.keyCode];
		    	control = false;
		    }
		};

	};
	
	this.checkWall = function (e){
        var p = getXY(e);
        if(p.x<limit.minCol || p.x>limit.maxCol || p.y<limit.minRow || p.y>limit.maxRow
                || this.objects[e])return true;
		return false;
	};

	this.move = function (){
		//// 当前蛇头位置，下一帧蛇头位置
		var preH = this.data[0], e = calcMove(preH, this.direction.head);
		//// 碰撞检测
		if(this.checkWall(e)){
			this.stop = true;
			return;
		}

		//// 蛇头更新
		this.data.unshift(e);
		this.objects[e] = this.objects[preH];
		controlDirection(this.objects[e], this.direction.head, e);

		//// 判断是否吃食物
		var f = this.eatFood(e);
		if(f){
			//// 食物移到蛇头后边，其他不变
			changePos(f, preH);
			this.objects[preH] = f;
		}else{
			//// 蛇尾的前一节移到蛇头后一节，蛇尾前移一节，中间不变， 原蛇尾数据去掉
			var tail = this.data[this.data.length - 2],
				preT = this.data[this.data.length - 1];
			
			changePos(this.objects[tail], preH);
			this.objects[preH] = this.objects[tail];
			this.objects[tail] = this.objects[preT];
			this.objects[this.data.pop()] = null;
			// 检测蛇尾方向
			this.controlTail();
			controlDirection(this.objects[tail], this.direction.tail, tail);
		}
		control = true;
	};
    
    

	//// 蛇尾方向控制
	this.controlTail = function (){
		var ptS = getXY(this.data[this.data.length - 1]), 
			ptT = getXY(this.data[this.data.length - 2]);
		/*
			direction : offset (X Y)
			↑ (0, -1)	→ (1, 0)		↓ (0, 1)		← (-1, 0)
		*/
		var offset = (ptT.x - ptS.x) * 2 + (ptT.y - ptS.y),
			d = { '-1':0, '2':1, '1':2, '-2':3 }[offset];
		if(d != opposite(this.direction.tail) ) this.direction.tail = opposite(d);
	};
    
	//// 改变物体方向
	function controlDirection(obj, direction, e){
		var pos = getPos(e);
		// 旋转后位置重置偏移，真实位置偏移
		var rx = 200 - config.cellW, ry = 200 - config.cellH,
			px = pos.x * 200 / config.cellW, py = pos.y * 200 / config.cellH;
		// param rotate translate scale for four direction
		var params = {
			0:{'R':90, T1:[0, ry], T2:[py, -px]}, 
			1:{'R':180, T1:[rx, ry], T2:[-px, -py]}, 
			2:{'R':-90, T1:[rx, 0], T2:[-py, px]}, 
			3:{'R':0, T1:[0, 0], T2:[px, py]}, 
		};

		var d = params[direction];
		obj.attr("transform",
				'rotate('+ d.R +', 100, 100)' +
				'translate('+ d.T1[0] +', '+ d.T1[1] +')' +
				'scale('+ config.cellW/200 +', '+ config.cellH/200 +')' +
				'translate('+ d.T2[0] +', '+ d.T2[1] +')'
		);
	}

	this.init();


	//// 根据方向计算偏移： 上下移一行，左右移一格
	function calcMove(e, direction){
		return e + [-config.col, 1, config.col, -1][direction];
	}
	function opposite(d){
		return (d + 2) % 4;
	} 
	function changePos(obj, e){
		var pos = getPos(e);
		obj.attr("transform", 'translate('+ (pos.x) +', '+ (pos.y) +')');
	}
	function getXY(e){
		return { x: e % config.col, y: parseInt(e / config.col) };
	}
	function getPos(e){
		return {
			x: config.left + config.cellW * (e % config.col),
			y: config.top + config.cellH * parseInt(e / config.col)
		};
	}

	////svg封闭图形
	//// 创建蛇尾
	function createTail(e){
		var tail = svg.append("g"), pos = getPos(e);
		tail.append("path")
				.attr("d", "M200 10 Q160 10 120 50 L120 50 C80 90 40 50 10 50 " +
						"L10 50 Q0 60 20 70 L20 70 C20 70 30 70 50 110 " +
						"L50 110 C70 140 90 150 120 140 S150 160 200 190 Z")

				.attr("fill", colorSnake);
		// controlDirection(tail, 3, 5);
		return tail;
	}
	//// 创建蛇头
	function createHead(e){
		var head = svg.append("g"), pos = getPos(e);
		head.append("path")
				.attr("d", "M200 30 C180 -20 60 0 10 60 " +
						"L10 60 Q0 100 10 140 " +
						"L10 140 C50 200 150 220 200 170 " +
						"L200 30 Q220 100 200 170 Z")
				.attr("fill", colorSnake);

		//// 椭圆大眼睛
		head.append("g").selectAll("ellipse")
				.data([[140, 50], [140, 150]])
				.enter().append("ellipse")
				.attr("cx", function(d){ return d[0]; })
				.attr("cy", function(d){ return d[1]; })
				.attr("rx", 60)
				.attr("ry", 42)
				.attr("fill", "#fff");
		//// 圆眼珠
		head.append("g").selectAll("circle")
				.data([[115, 50], [115, 150]])
				.enter().append("circle")
				.attr("cx", function(d){ return d[0]; })
				.attr("cy", function(d){ return d[1]; })
				.attr("r", 30)
				.attr("fill", "#000");

		// controlDirection(head, 1, 1);
		return head;
	}
	//// 创建一格身体
	function createBody(e){
		var pos = getPos(e);
		var body = bodyGroup.append("rect")
				.attr('x', 0)
				.attr('y', 0)
				.attr('width', config.cellW)
				.attr('height', config.cellH)
				.attr("fill", colorSnake);
		body.attr('transform', 'translate('+ pos.x +', '+ pos.y +')');
		return body;
	}
}





//-------------------------------------Class Helix---------------------------------------------
function Helix(colSize, rowSize){
	var col = colSize || 5, row = rowSize || 5;
	var limit = { minCol:0, minRow:0, maxCol:col - 1, maxRow:row -1 };
	var index = 0, direction = 0;
	var datas = { 0:{ x:0, y:0 } };
	this.elements = { 0:0 };   // 螺旋顺序索引：按行顺序索引
    this.layers = [];           // 一整圈完成分界点
	
	this.move = function(d){
		var tp = {
			0: function(){ datas[index] = { x:datas[index - 1].x + 1, y:datas[index - 1].y }; },
			1: function(){ datas[index] = { x:datas[index - 1].x, y:datas[index - 1].y + 1 }; },
			2: function(){ datas[index] = { x:datas[index - 1].x - 1, y:datas[index - 1].y }; },
			3: function(){ datas[index] = { x:datas[index - 1].x, y:datas[index - 1].y - 1 }; },
		}[d]();
		//// 格子方块索引 (对应按行顺序排列位置)
		this.elements[index] = datas[index].y * col + datas[index].x;
	};
	
	//// 转向时缩小相关边界
	this.chenge = function(d){
		var tp = {
			0: function(){ limit.minRow++; },
			1: function(){ limit.maxCol--; },
			2: function(){ limit.maxRow--; },
			3: function(){ limit.minCol++; },
		}[d]();
        //// 走完完整一圈分界点判断
        if(d==3 && (datas[index - 1].y==limit.minRow))this.layers.push(index - 1);
	};
    
	while(index++< col * row){
		var p = datas[index - 1], d = direction % 4;
		//// 判断转向
		if((p.x==limit.maxCol && d==0)|| (p.y==limit.maxRow && d==1) || 
			(p.x==limit.minCol && d==2) || (p.y==limit.minRow && d==3)){
			this.chenge(direction++ % 4);
		}
		//// 在当前方向上移动
		this.move(direction % 4);
	}
}
//-------------------------------------End Class Helix---------------------------------------------

	
function cout(obj){ console.log(obj); }

</script>

</body>
</html>